package biolabs.util;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.Arrays;

public class Types {

	public static ParameterizedType newParameterizedType(final Class<?> rawType, final Type[] typeArgs) {
		return newParameterizedType(rawType, typeArgs, null);
	}

	public static ParameterizedType newParameterizedType(final Class<?> rawType, final Type[] typeArgs, final Type ownerType) {
		if(rawType == null)
			throw new NullPointerException("rawType is null");
		if(typeArgs == null)
			throw new NullPointerException("typeArgs is null");

		return new ParameterizedType() {

			Type[] args = Arrays.copyOf(typeArgs, typeArgs.length);

			@Override
			public Type[] getActualTypeArguments() {
				return args;
			}

			@Override
			public Type getOwnerType() {
				return ownerType;
			}

			@Override
			public Type getRawType() {
				return rawType;
			}

			@Override
			public boolean equals(Object o) {
				if(!(o instanceof ParameterizedType))
					return false;

				ParameterizedType pt = (ParameterizedType) o;
				if(rawType.equals(pt.getRawType()))
					return false;
				if(pt.getOwnerType() != null)
					return false;
				return Arrays.equals(args, pt.getActualTypeArguments());
			}

			@Override
			public String toString() {
				StringBuilder sb = new StringBuilder();
				sb.append("type ").append(rawType.getName());
				if(args.length > 0) {
					sb.append("<").append(args[0]);
					for(int i=1; i<args.length; ++i)
						sb.append(args[i]);
					sb.append(">");
				}
				return sb.toString();
			}

		};
	}

	public static Type[] resolveActualTypeArguments(Class<?> sup, Class<?> sub) {
		return resolveActualTypeArguments(sup, sub, sub.getTypeParameters());
	}

	public static Type[] resolveActualTypeArguments(Class<?> sup, Class<?> sub, Type[] subArgs) {

		if(!sup.isAssignableFrom(sub))
			throw new IllegalArgumentException(sup + " is not assignable from " + sub);

		Type[] res = _resolveActualTypeArguments(sup, sub, subArgs);
		assert res.length == sup.getTypeParameters().length;

		return res;
	}

	public static Type[] resolveActualTypeArguments(Class<?> sup, Type sub) {
		if(sub instanceof Class)
			return resolveActualTypeArguments(sup, (Class<?>) sub);
		else if(sub instanceof ParameterizedType)
			return resolveActualTypeArguments(sup, (Class<?>)((ParameterizedType) sub).getRawType(), ((ParameterizedType) sub).getActualTypeArguments());
		else
			throw new IllegalArgumentException("The subtype " + sub + " is neither Class nor ParameterizedType");
	}

	private static Type[] _resolveActualTypeArguments(Class<?> sup, Class<?> sub, Type[] subArgs) {
		if(sup == sub)
			return subArgs;
		assert sup.isAssignableFrom(sub) : sup + " is not assignable from " + sub;

		// get the supertypes of sub
		Type superclass = sub.getGenericSuperclass();
		Type[] superifaces = sub.getGenericInterfaces();
		Type[] supertypes = new Type[1+superifaces.length];
		supertypes[0] = superclass;
		System.arraycopy(superifaces, 0, supertypes, 1, superifaces.length);

		// choose the one that is a subtype of sup and recurse
		for(Type t: supertypes) {
			if(t instanceof Class<?>) {
				Class<?> clazz = (Class<?>) t;
				assert clazz.getTypeParameters().length == 0;
				if(sup.isAssignableFrom(clazz))
					return resolveActualTypeArguments(sup, clazz, new Type[0]);
			} else if(t instanceof ParameterizedType) {
				ParameterizedType type = (ParameterizedType) t;
				Class<?> clazz = (Class<?>) type.getRawType();
				if(sup.isAssignableFrom(clazz)) {
					Type[] actuals = getActualTypeArguments(type, sub.getTypeParameters(), subArgs);
					return resolveActualTypeArguments(sup, clazz, actuals);
				}
			} else {
				assert false : "Generic supertype " + t + " of " + sub + " is neither Class<?> nor ParameterizedType";
			}
		}
		throw new AssertionError("unreachable code");
	}

	public static Type[] resolveConcreteTypeArguments(Class<?> sup, Class<?> sub) {
		return resolveConcreteTypeArguments(sup, sub, sub.getTypeParameters());
	}

	public static Type[] resolveConcreteTypeArguments(Class<?> sup, Class<?> sub, Type[] subArgs) {

		Type[] res = resolveActualTypeArguments(sup, sub, subArgs);

		// check that all type parameters (incl. nested) have been resolved
		checkConcreteness(sup, res);

		return res;
	}

	private static Type[] getActualTypeArguments(ParameterizedType type, TypeVariable<?>[] refFormals, Type[] refActuals) {
		Type[] actuals = type.getActualTypeArguments();

		// substitute any type variables
		for(int i=0; i<actuals.length; ++i) {
			Type arg = actuals[i];
			if(arg instanceof TypeVariable<?>) {
				String name = ((TypeVariable<?>)arg).getName();
				for(int j=0; j<refFormals.length; ++j) {
					TypeVariable<?> param = refFormals[j];
					if(param.getName().equals(name)) {
						actuals[i] = refActuals[j];
						break;
					}
				}
			} else if(arg instanceof ParameterizedType) {
				final ParameterizedType ptype = (ParameterizedType)arg;
				// substitute any type variables of the parameterized type
				final Type[] args = getActualTypeArguments(ptype, refFormals, refActuals);
				actuals[i] = newParameterizedType((Class<?>) ptype.getRawType(), args, ptype.getOwnerType());
			}
		}
		return actuals;
	}

	private static void checkConcreteness(Class<?> clazz, Type[] resolvedArgs) {
		for(int i=0; i<resolvedArgs.length; ++i) {
			Type t = resolvedArgs[i];
			if(t instanceof TypeVariable<?>) {
				throw new IllegalArgumentException("Type parameter "
						+ clazz.getTypeParameters()[i].getName() + " of "
						+ clazz + " resolved as type variable "
						+ ((TypeVariable<?>)t).getName());
			} else if(t instanceof ParameterizedType) {
				ParameterizedType pt = (ParameterizedType) t;
				checkConcreteness((Class<?>) pt.getRawType(), pt.getActualTypeArguments());
			}
		}
	}
}
